Passed
Pull Request — master (#2)
by Muhammad Dyas
02:13
created

index.ts ➔ showConfigurationForm   F

Complexity

Conditions 34

Size

Total Lines 19
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 12
dl 0
loc 19
rs 0
c 0
b 0
f 0
cc 34

How to fix   Complexity   

Complexity

Complex classes like index.ts ➔ showConfigurationForm often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
import {HttpFunction} from '@google-cloud/functions-framework/build/src/functions';
2
3
import {
4
  buildConfigurationForm,
5
} from './config-form';
6
import {buildVoteCard} from './vote-card';
7
import {saveVotes} from './helpers/vote';
8
import {buildAddOptionForm} from './add-option-form';
9
import {callMessageApi} from './helpers/api';
10
import {addOptionToState} from './helpers/option';
11
import {buildActionResponseStatus} from './helpers/response';
12
import {MAX_NUM_OF_OPTIONS} from './config/default';
13
import {splitMessage} from './helpers/utils';
14
import {chat_v1 as chatV1} from 'googleapis/build/src/apis/chat/v1';
15
import {Voter, Votes} from './helpers/interfaces';
16
import {PollCard} from './cards/PollCard';
17
import {CommandHandler} from './handlers/CommandHandler';
18
import {MessageHandler} from './handlers/MessageHandler';
19
20
export const app: HttpFunction = async (req, res) => {
21
  if (!(req.method === 'POST' && req.body)) {
22
    res.status(400).send('');
23
  }
24
  const buttonCard: chatV1.Schema$CardWithId = {
25
    'cardId': 'welcome-card',
26
    'card': {
27
      'sections': [
28
        {
29
          'widgets': [
30
            {
31
              'buttonList': {
32
                'buttons': [
33
                  {
34
                    'text': 'Create Poll',
35
                    'onClick': {
36
                      'action': {
37
                        'function': 'show_form',
38
                        'interaction': 'OPEN_DIALOG',
39
                        'parameters': [],
40
                      },
41
                    },
42
                  },
43
                  {
44
                    'text': 'Terms and Conditions',
45
                    'onClick': {
46
                      'openLink': {
47
                        'url': 'https://absolute-poll.yaskur.com/terms-and-condition',
48
                      },
49
                    },
50
                  },
51
                  {
52
                    'text': 'Contact Us',
53
                    'onClick': {
54
                      'openLink': {
55
                        'url': 'https://absolute-poll.yaskur.com/contact-us',
56
                      },
57
                    },
58
                  },
59
                ],
60
              },
61
            },
62
          ],
63
        },
64
      ],
65
    },
66
  };
67
  const event = req.body;
68
  console.log(event.type,
69
    event.common?.invokedFunction || event.message?.slashCommand?.commandId || event.message?.argumentText,
70
    event.user.displayName, event.user.email, event.space.type, event.space.name);
71
  console.log(JSON.stringify(event.message.cardsV2));
72
  console.log(JSON.stringify(event.user));
73
  let reply: chatV1.Schema$Message = {
74
    thread: event.message.thread,
75
    actionResponse: {
76
      type: 'NEW_MESSAGE',
77
    },
78
    text: 'Hi! To create a poll, you can use the */poll* command. \n \n' +
79
      'Alternatively, you can create poll by mentioning me with question and answers. ' +
80
      'e.g *@Absolute Poll "Your Question" "Answer 1" "Answer 2"*',
81
  };
82
  // Dispatch slash and action events
83
  if (event.type === 'MESSAGE') {
84
    const message = event.message;
85
    if (message.text) {
86
      const argument = event.message?.argumentText?.trim().toLowerCase();
87
88
      reply = {
89
        thread: event.message.thread,
90
        actionResponse: {
91
          type: 'NEW_MESSAGE',
92
        },
93
        text: 'Hi! To create a poll, you can use the */poll* command. \n \n' +
94
          'Alternatively, you can create poll by mentioning me with question and answers. ' +
95
          'e.g *@Absolute Poll "Your Question" "Answer 1" "Answer 2"*',
96
      };
97
      const choices = splitMessage(argument);
98
      if (choices.length > 2) {
99
        const pollCard = buildVoteCard({
100
          choiceCreator: undefined,
101
          topic: choices.shift() ?? '',
102
          author: event.user,
103
          choices: choices,
104
          votes: {},
105
          anon: false,
106
          optionable: true,
107
        });
108
        const message = {
109
          cardsV2: [pollCard],
110
        };
111
        reply = {
112
          thread: event.message.thread,
113
          actionResponse: {
114
            type: 'NEW_MESSAGE',
115
          },
116
          ...message,
117
        };
118
      }
119
120
      if (argument === 'help') {
121
        reply.text = 'Hi there! I can help you create polls to enhance collaboration and efficiency ' +
122
          'in decision-making using Google Chat™.\n' +
123
          '\n' +
124
          'Below is an example commands:\n' +
125
          '`/poll` - You will need to fill out the topic and answers in the form that will be displayed.\n' +
126
          '`/poll "Which is the best country to visit" "Indonesia"` - to create a poll with ' +
127
          '"Which is the best country to visit" as the topic and "Indonesia" as the answer\n' +
128
          '\n' +
129
          'We hope you find our service useful and please don\'t hesitate to contact us ' +
130
          'if you have any questions or concerns.';
131
        reply.cardsV2 = [buttonCard];
132
      } else if (argument === 'test') {
133
        reply.text = 'test search on <a href=\'http://www.google.com\'>google</a> (https://google.com)[https://google.com]';
134
      }
135
    }
136
    if (message.slashCommand?.commandId) {
137
      reply = new CommandHandler(event).process();
138
    }
139
  } else if (event.type === 'CARD_CLICKED') {
140
    const action = event.common?.invokedFunction;
141
    if (action === 'start_poll') {
142
      reply = await startPoll(event);
143
    } else if (action === 'vote') {
144
      reply = recordVote(event);
145
    } else if (action === 'add_option_form') {
146
      reply = addOptionForm(event);
147
    } else if (action === 'add_option') {
148
      reply = await saveOption(event);
149
    } else if (action === 'show_form') {
150
      // todo: show form using new card generator
151
    }
152
  } else if (event.type === 'ADDED_TO_SPACE') {
153
    const message: chatV1.Schema$Message = {
154
      text: undefined,
155
      cardsV2: undefined,
156
    };
157
    const spaceType = event.space.type;
158
    if (spaceType === 'ROOM') {
159
      message.text = 'Hi there! I\'d be happy to assist you in creating polls to improve collaboration and ' +
160
        'decision-making efficiency on Google Chat™.\n' +
161
        '\n' +
162
        'To create a poll, simply use the */poll* command or click on the "Create Poll" button below. ' +
163
        'You can also test our app in a direct message if you prefer.\n' +
164
        '\n' +
165
        'Alternatively, you can ' +
166
        'You can also test our app in a direct message if you prefer.\n' +
167
        '\n' +
168
        'We hope you find our service useful and please don\'t hesitate to contact us ' +
169
        'if you have any questions or concerns.';
170
    } else if (spaceType === 'DM') {
171
      message.text = 'Hey there! ' +
172
        'Before creating a poll in a group space, you can test it out here in a direct message.\n' +
173
        '\n' +
174
        'To create a poll, you can use the */poll* command or click on the "Create Poll" button below.\n' +
175
        '\n' +
176
        'Thank you for using our bot. We hope that it will prove to be a valuable tool for you and your team.\n' +
177
        '\n' +
178
        'Don\'t hesitate to reach out if you have any questions or concerns in the future.' +
179
        ' We are always here to help you and your team';
180
    }
181
182
    message.cardsV2 = [buttonCard];
183
184
    reply = {
185
      actionResponse: {
186
        type: 'NEW_MESSAGE',
187
      },
188
      ...message,
189
    };
190
  }
191
  res.json(reply);
192
};
193
194
195
/**
196
 * Handle the custom start_poll action.
197
 *
198
 * @param {object} event - chat event
199
 * @returns {object} Response to send back to Chat
200
 */
201
async function startPoll(event: chatV1.Schema$DeprecatedEvent) {
202
  // Get the form values
203
  const formValues = event.common?.formInputs;
204
  const topic = formValues?.['topic']?.stringInputs?.value?.[0]?.trim() ?? '';
205
  const isAnonymous = formValues?.['is_anonymous']?.stringInputs?.value?.[0] === '1';
206
  const allowAddOption = formValues?.['allow_add_option']?.stringInputs?.value?.[0] === '1';
207
  const choices = [];
208
  const votes: Votes = {};
209
210
  for (let i = 0; i < MAX_NUM_OF_OPTIONS; ++i) {
211
    const choice = formValues?.[`option${i}`]?.stringInputs?.value?.[0]?.trim();
212
    if (choice) {
213
      choices.push(choice);
214
      votes[i] = [];
215
    }
216
  }
217
218
  if (!topic || choices.length === 0) {
219
    // Incomplete form submitted, rerender
220
    const dialog = buildConfigurationForm({
221
      topic,
222
      choices,
223
    });
224
    return {
225
      actionResponse: {
226
        type: 'DIALOG',
227
        dialogAction: {
228
          dialog: {
229
            body: dialog,
230
          },
231
        },
232
      },
233
    };
234
  }
235
  const pollCard = new PollCard({
236
    topic: topic, choiceCreator: undefined,
237
    author: event.user,
238
    choices: choices,
239
    votes: votes,
240
    anon: isAnonymous,
241
    optionable: allowAddOption,
242
  }).make();
243
  // Valid configuration, make the voting card to display in the space
244
  const message = {
245
    cardsV2: [pollCard],
246
  };
247
  const request = {
248
    parent: event.space?.name,
249
    requestBody: message,
250
  };
251
  const apiResponse = await callMessageApi('create', request);
252
  if (apiResponse) {
253
    return buildActionResponseStatus('Poll started.', 'OK');
254
  } else {
255
    return buildActionResponseStatus('Failed to start poll.', 'UNKNOWN');
256
  }
257
}
258
259
/**
260
 * Handle the custom vote action. Updates the state to record
261
 * the user's vote then rerenders the card.
262
 *
263
 * @param {object} event - chat event
264
 * @returns {object} Response to send back to Chat
265
 */
266
function recordVote(event: chatV1.Schema$DeprecatedEvent) {
267
  const parameters = event.common?.parameters;
268
  if (!(parameters?.['index'])) {
269
    throw new Error('Index Out of Bounds');
270
  }
271
  const choice = parseInt(parameters['index']);
272
  const userId = event.user?.name ?? '';
273
  const userName = event.user?.displayName ?? '';
274
  const voter: Voter = {uid: userId, name: userName};
275
  const state = JSON.parse(parameters['state']);
276
277
  // Add or update the user's selected option
278
  state.votes = saveVotes(choice, voter, state.votes, state.anon);
279
280
  const card = buildVoteCard(state);
281
  return {
282
    thread: event.message?.thread,
283
    actionResponse: {
284
      type: 'UPDATE_MESSAGE',
285
    },
286
    cardsV2: [card],
287
  };
288
}
289
290
/**
291
 * Opens and starts a dialog that allows users to add details about a contact.
292
 *
293
 * @param {object} event the event object from Google Chat.
294
 *
295
 * @returns {object} open a dialog.
296
 */
297
function addOptionForm(event: chatV1.Schema$DeprecatedEvent) {
298
  const card = event.message!.cardsV2?.[0]?.card;
299
  // @ts-ignore: because too long
300
  const stateJson = (card.sections[0].widgets[0].decoratedText?.button?.onClick?.action?.parameters[0].value || card.sections[1].widgets[0].decoratedText?.button?.onClick?.action?.parameters[0].value) ?? '';
301
  const state = JSON.parse(stateJson);
302
  const dialog = buildAddOptionForm(state);
303
  return {
304
    actionResponse: {
305
      type: 'DIALOG',
306
      dialogAction: {
307
        dialog: {
308
          body: dialog,
309
        },
310
      },
311
    },
312
  };
313
}
314
;
315
316
/**
317
 * Handle the custom vote action. Updates the state to record
318
 * the user's vote then rerenders the card.
319
 *
320
 * @param {chatV1.Schema$DeprecatedEvent} event - chat event
321
 * @returns {object} Response to send back to Chat
322
 */
323
async function saveOption(event: chatV1.Schema$DeprecatedEvent) {
324
  const userName = event.user?.displayName ?? '';
325
  const state = getEventPollState(event);
326
  const formValues = event.common?.formInputs;
327
  const optionValue = formValues?.['value']?.stringInputs?.value?.[0]?.trim() || '';
328
  addOptionToState(optionValue, state, userName);
329
330
  const card = buildVoteCard(state);
331
  const message = {
332
    cardsV2: [card],
333
  };
334
  const request = {
335
    name: event.message!.name,
336
    requestBody: message,
337
    updateMask: 'cardsV2',
338
  };
339
  const apiResponse = await callMessageApi('update', request);
340
  if (apiResponse) {
341
    return buildActionResponseStatus('Option is added', 'OK');
342
  } else {
343
    return buildActionResponseStatus('Failed to add option.', 'UNKNOWN');
344
  }
345
}
346
347
function getEventPollState(event: chatV1.Schema$DeprecatedEvent) {
348
  const parameters = event.common?.parameters;
349
  const state = parameters?.['state'];
350
  if (!state) {
351
    throw new ReferenceError('no valid state in the event');
352
  }
353
  return JSON.parse(state);
354
}
355